/* @flow */

import config from "../config";
import Watcher from "../observer/watcher";
import Dep, { pushTarget, popTarget } from "../observer/dep";
import { isUpdatingChildComponent } from "./lifecycle";

import {
	set,
	del,
	observe,
	defineReactive,
	toggleObserving,
} from "../observer/index";

import {
	warn,
	bind,
	noop,
	hasOwn,
	hyphenate,
	isReserved,
	handleError,
	nativeWatch,
	validateProp,
	isPlainObject,
	isServerRendering,
	isReservedAttribute,
	invokeWithErrorHandling,
} from "../util/index";

const sharedPropertyDefinition = {
	enumerable: true,
	configurable: true,
	get: noop,
	set: noop,
};

// target vm
// sourceKey _data _props
// key 某个属性名
export function proxy(target: Object, sourceKey: string, key: string) {
	// sharedPropertyDefinition.get = function proxyGetter() {
	// 	return this[sourceKey][key];
	// };
	// sharedPropertyDefinition.set = function proxySetter(val) {
	// 	this[sourceKey][key] = val;
	// };
	// Object.defineProperty(target, key, sharedPropertyDefinition);
	Object.defineProperty(vm, key, {
		enumerable: true,
		configurable: true,
		get: function proxyGetter() {
			return this[`_data`][key];
		},
		set: function proxySetter(val) {
			this[`_data`][key] = val;
		},
	});
}

export function initState(vm: Component) {
	vm._watchers = [];
	const opts = vm.$options;
	// 数据代理
	// 代理props
	if (opts.props) initProps(vm, opts.props);
	// 代理methods
	if (opts.methods) initMethods(vm, opts.methods);
	// 代理data
	if (opts.data) {
		initData(vm);
	} else {
		observe((vm._data = {}), true /* asRootData */);
	}
	// 代理computed
	if (opts.computed) initComputed(vm, opts.computed);
	// 初始化watch
	if (opts.watch && opts.watch !== nativeWatch) {
		initWatch(vm, opts.watch);
	}
}

function initProps(vm: Component, propsOptions: Object) {
	const propsData = vm.$options.propsData || {};
	const props = (vm._props = {});
	// cache prop keys so that future props updates can iterate using Array
	// instead of dynamic object key enumeration.
	const keys = (vm.$options._propKeys = []);
	const isRoot = !vm.$parent;
	// root instance props should be converted
	if (!isRoot) {
		toggleObserving(false);
	}
	for (const key in propsOptions) {
		keys.push(key);
		const value = validateProp(key, propsOptions, propsData, vm);
		/* istanbul ignore else */
		if (process.env.NODE_ENV !== "production") {
			const hyphenatedKey = hyphenate(key);
			if (
				isReservedAttribute(hyphenatedKey) ||
				config.isReservedAttr(hyphenatedKey)
			) {
				warn(
					`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
					vm
				);
			}
			defineReactive(props, key, value, () => {
				if (!isRoot && !isUpdatingChildComponent) {
					warn(
						`Avoid mutating a prop directly since the value will be ` +
							`overwritten whenever the parent component re-renders. ` +
							`Instead, use a data or computed property based on the prop's ` +
							`value. Prop being mutated: "${key}"`,
						vm
					);
				}
			});
		} else {
			defineReactive(props, key, value);
		}
		// static props are already proxied on the component's prototype
		// during Vue.extend(). We only need to proxy props defined at
		// instantiation here.
		if (!(key in vm)) {
			proxy(vm, `_props`, key);
		}
	}
	toggleObserving(true);
}

function initData(vm: Component) {
	let data = vm.$options.data;
	// 原数据
	data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
	if (!isPlainObject(data)) {
		data = {};
		process.env.NODE_ENV !== "production" &&
			warn(
				"data functions should return an object:\n" +
					"https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
				vm
			);
	}
	// proxy data on instance
	const keys = Object.keys(data);
	const props = vm.$options.props;
	const methods = vm.$options.methods;
	let i = keys.length;
	while (i--) {
		const key = keys[i];
		if (process.env.NODE_ENV !== "production") {
			if (methods && hasOwn(methods, key)) {
				warn(
					`Method "${key}" has already been defined as a data property.`,
					vm
				);
			}
		}
		if (props && hasOwn(props, key)) {
			process.env.NODE_ENV !== "production" &&
				warn(
					`The data property "${key}" is already declared as a prop. ` +
						`Use prop default value instead.`,
					vm
				);
		} else if (!isReserved(key)) {
			// 数据代理
			proxy(vm, `_data`, key);
		}
	}
	// 数据劫持
	observe(data, true /* asRootData */);
}

export function getData(data: Function, vm: Component): any {
	// #7573 disable dep collection when invoking data getters
	pushTarget();
	try {
		return data.call(vm, vm);
	} catch (e) {
		handleError(e, vm, `data()`);
		return {};
	} finally {
		popTarget();
	}
}

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {
	// $flow-disable-line
	const watchers = (vm._computedWatchers = Object.create(null));
	// computed properties are just getters during SSR
	const isSSR = isServerRendering();

	for (const key in computed) {
		const userDef = computed[key];
		const getter = typeof userDef === "function" ? userDef : userDef.get;
		if (process.env.NODE_ENV !== "production" && getter == null) {
			warn(`Getter is missing for computed property "${key}".`, vm);
		}

		if (!isSSR) {
			// create internal watcher for the computed property.
			watchers[key] = new Watcher(
				vm,
				getter || noop,
				noop,
				computedWatcherOptions
			);
		}

		// component-defined computed properties are already defined on the
		// component prototype. We only need to define computed properties defined
		// at instantiation here.
		if (!(key in vm)) {
			defineComputed(vm, key, userDef);
		} else if (process.env.NODE_ENV !== "production") {
			if (key in vm.$data) {
				warn(`The computed property "${key}" is already defined in data.`, vm);
			} else if (vm.$options.props && key in vm.$options.props) {
				warn(
					`The computed property "${key}" is already defined as a prop.`,
					vm
				);
			} else if (vm.$options.methods && key in vm.$options.methods) {
				warn(
					`The computed property "${key}" is already defined as a method.`,
					vm
				);
			}
		}
	}
}

export function defineComputed(
	target: any,
	key: string,
	userDef: Object | Function
) {
	const shouldCache = !isServerRendering();
	if (typeof userDef === "function") {
		sharedPropertyDefinition.get = shouldCache
			? createComputedGetter(key)
			: createGetterInvoker(userDef);
		sharedPropertyDefinition.set = noop;
	} else {
		sharedPropertyDefinition.get = userDef.get
			? shouldCache && userDef.cache !== false
				? createComputedGetter(key)
				: createGetterInvoker(userDef.get)
			: noop;
		sharedPropertyDefinition.set = userDef.set || noop;
	}
	if (
		process.env.NODE_ENV !== "production" &&
		sharedPropertyDefinition.set === noop
	) {
		sharedPropertyDefinition.set = function () {
			warn(
				`Computed property "${key}" was assigned to but it has no setter.`,
				this
			);
		};
	}
	Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter(key) {
	return function computedGetter() {
		const watcher = this._computedWatchers && this._computedWatchers[key];
		if (watcher) {
			if (watcher.dirty) {
				watcher.evaluate();
			}
			if (Dep.target) {
				watcher.depend();
			}
			return watcher.value;
		}
	};
}

function createGetterInvoker(fn) {
	return function computedGetter() {
		return fn.call(this, this);
	};
}

function initMethods(vm: Component, methods: Object) {
	const props = vm.$options.props;
	for (const key in methods) {
		if (process.env.NODE_ENV !== "production") {
			if (typeof methods[key] !== "function") {
				warn(
					`Method "${key}" has type "${typeof methods[
						key
					]}" in the component definition. ` +
						`Did you reference the function correctly?`,
					vm
				);
			}
			if (props && hasOwn(props, key)) {
				warn(`Method "${key}" has already been defined as a prop.`, vm);
			}
			if (key in vm && isReserved(key)) {
				warn(
					`Method "${key}" conflicts with an existing Vue instance method. ` +
						`Avoid defining component methods that start with _ or $.`
				);
			}
		}
		vm[key] =
			typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
	}
}

function initWatch(vm: Component, watch: Object) {
	for (const key in watch) {
		const handler = watch[key];
		if (Array.isArray(handler)) {
			for (let i = 0; i < handler.length; i++) {
				createWatcher(vm, key, handler[i]);
			}
		} else {
			createWatcher(vm, key, handler);
		}
	}
}

function createWatcher(
	vm: Component,
	expOrFn: string | Function,
	handler: any,
	options?: Object
) {
	if (isPlainObject(handler)) {
		options = handler;
		handler = handler.handler;
	}
	if (typeof handler === "string") {
		handler = vm[handler];
	}
	return vm.$watch(expOrFn, handler, options);
}

export function stateMixin(Vue: Class<Component>) {
	// flow somehow has problems with directly declared definition object
	// when using Object.defineProperty, so we have to procedurally build up
	// the object here.
	const dataDef = {};
	dataDef.get = function () {
		return this._data;
	};
	const propsDef = {};
	propsDef.get = function () {
		return this._props;
	};
	if (process.env.NODE_ENV !== "production") {
		dataDef.set = function () {
			warn(
				"Avoid replacing instance root $data. " +
					"Use nested data properties instead.",
				this
			);
		};
		propsDef.set = function () {
			warn(`$props is readonly.`, this);
		};
	}
	Object.defineProperty(Vue.prototype, "$data", dataDef);
	Object.defineProperty(Vue.prototype, "$props", propsDef);

	Vue.prototype.$set = set;
	Vue.prototype.$delete = del;

	Vue.prototype.$watch = function (
		expOrFn: string | Function,
		cb: any,
		options?: Object
	): Function {
		const vm: Component = this;
		if (isPlainObject(cb)) {
			return createWatcher(vm, expOrFn, cb, options);
		}
		options = options || {};
		options.user = true;
		const watcher = new Watcher(vm, expOrFn, cb, options);
		if (options.immediate) {
			const info = `callback for immediate watcher "${watcher.expression}"`;
			pushTarget();
			invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
			popTarget();
		}
		return function unwatchFn() {
			watcher.teardown();
		};
	};
}
